﻿//-----------------------------------------------------------------------
// <copyright file="DataBagObjectSerialization.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace TileTool.DataBag
{
    using System;
    using System.Reflection;
    using TileTool.Common.Math;

    /// <summary>
    /// Utility class to handle serialization from data bag to object and
    /// object to data bag.
    /// </summary>
    public static class DataBagObjectSerialization
    {
        /// <summary>
        /// Convert the passed data bag to an object.
        /// 
        /// This function will find all read / write properties
        /// on the object and attempt to find a corresponding data bag value
        /// with the same name. If we don't have a value, we ignore the property.
        /// </summary>
        /// <typeparam name="T">Type of object to create.</typeparam>
        /// <param name="bag">Databag to populate the object from.</param>
        /// <returns>Returns the populated object.</returns>
        public static T DataBagToObject<T>(IDataBag bag) where T : class, new()
        {
            if (bag == null)
            {
                throw new ArgumentNullException("bag", "Data bag must be non-null.");
            }

            T resObject = new T();
            Type objectType = typeof(T);
            PropertyInfo[] properties = objectType.GetProperties();

            foreach (PropertyInfo property in properties)
            {
                // We must be able to read and write the property.
                if (!property.CanWrite || !property.CanRead)
                {
                    continue;
                }

                // If the object has a type on it that we don't support, we
                // ignore it.
                if (!DataBagValueTypeUtility.IsValidType(property.PropertyType))
                {
                    continue;
                }

                // If we don't have a value in the bag with the same name as the 
                // property we can't do anything.
                IDataBagValue bagValue;
                if (!bag.TryGetValue(property.Name, out bagValue))
                {
                    continue;
                }

                object objectValue = bagValue.ObjectValue;
                if (objectValue == null)
                {
                    throw new InvalidOperationException("Data bag value return a null object value.");
                }

                property.SetValue(resObject, objectValue);
            }

            return resObject;
        }

        /// <summary>
        /// Convert the passed object to a data bag.
        /// 
        /// This function will inspect all read / write properties on the object
        /// and convert the property to a data bag item if it is a supported
        /// data bag type.
        /// </summary>
        /// <typeparam name="T">Type of object that is being converted.</typeparam>
        /// <param name="obj">Object to convert.</param>
        /// <returns>Returns the object as a data bag.</returns>
        public static IDataBag ObjectToDataBag<T>(T obj) where T : class
        {
            if (obj == null)
            {
                throw new ArgumentNullException("obj", "Object must be non-null.");
            }

            DataBag resBag = new DataBag();
            Type objectType = obj.GetType();
            PropertyInfo[] properties = objectType.GetProperties();

            foreach (PropertyInfo property in properties)
            {
                // We must be able to read and write the property.
                if (!property.CanWrite || !property.CanRead)
                {
                    continue;
                }

                // If the object has a type on it that we don't support, we
                // can't serialize.
                if (!DataBagValueTypeUtility.IsValidType(property.PropertyType))
                {
                    continue;
                }

                object objectValue = property.GetValue(obj);
                if (objectValue == null)
                {
                    continue;
                }

                DataBagValueFromObject(resBag, property.Name, objectValue);
            }

            return resBag;
        }

        /// <summary>
        /// Convert the passed object into a data bag value.
        /// </summary>
        /// <param name="bag">Data bag to add the new value into.</param>
        /// <param name="name">Name of the new data bag value.</param>
        /// <param name="value">Value to create a databag value from.</param>
        private static void DataBagValueFromObject(DataBag bag, string name, object value)
        {
            DataBagValueType type = DataBagValueTypeUtility.GetDataBagValueTypeFromType(value.GetType());

            switch (type)
            {
                case DataBagValueType.Int:
                    bag.CreateValue<int>(name, (int)value);
                    break;
                case DataBagValueType.UInt:
                    bag.CreateValue<uint>(name, (uint)value);
                    break;
                case DataBagValueType.Long:
                    bag.CreateValue<long>(name, (long)value);
                    break;
                case DataBagValueType.ULong:
                    bag.CreateValue<ulong>(name, (ulong)value);
                    break;
                case DataBagValueType.Float:
                    bag.CreateValue<float>(name, (float)value);
                    break;
                case DataBagValueType.String:
                    bag.CreateValue<string>(name, (string)value);
                    break;
                case DataBagValueType.ColorRGBA:
                    bag.CreateValue<ColorRGBA>(name, (ColorRGBA)value);
                    break;
                case DataBagValueType.Vector2D:
                    bag.CreateValue<Vector2D>(name, (Vector2D)value);
                    break;
                case DataBagValueType.Matrix2D:
                    bag.CreateValue<Matrix2D>(name, (Matrix2D)value);
                    break;
                case DataBagValueType.Size2D:
                    bag.CreateValue<Size2D>(name, (Size2D)value);
                    break;
                default:
                    throw new InvalidOperationException("Unknown data bag value type: " + type.ToString());
            }
        }
    }
}
